﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.CodeDom;
using System.Text.RegularExpressions;
using System.CodeDom.Compiler;
using System.Reflection;
using System.IO;
using System.Diagnostics;

namespace EasyTemplates
{
    class TemplateCompiler : ITemplateCompiler
    {
        #region Public interface
        public Template Template { get; set; }

        public Type GenerateType()
        {
            string className = "TemplateGenerator" + Guid.NewGuid().ToString().Replace("-", "");
            CodeCompileUnit compileUnit = GenerateCode( className );
            return Compile(compileUnit, className);
        }
        #endregion

        #region Code generation
        const string GeneratedNamespace = "EasyTemplates";

        CodeCompileUnit GenerateCode(string className)
        {
            CodeCompileUnit compileUnit = new CodeCompileUnit();
            CodeNamespace codeNamespace = new CodeNamespace(GeneratedNamespace);

            CodeTypeDeclaration mainClass = new CodeTypeDeclaration(className);
            codeNamespace.Types.Add(mainClass);
            mainClass.Name = className;
            mainClass.Attributes = MemberAttributes.Public;
            mainClass.BaseTypes.Add(typeof(GeneratedTemplate));

            mainClass.Members.AddRange(ParametersCodeDoms.ToArray());
            mainClass.Members.Add(GenerateMethodCodeDom);

            compileUnit.Namespaces.Add(codeNamespace);
            compileUnit.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().CodeBase);

            return compileUnit;
        }

        Type Compile(CodeCompileUnit compileUnit, string className)
        {
            string language =
                Template.Language == TemplateLanguages.CSharp ?
                "CSharp" :
                "VisualBasic";

            CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");

            StringBuilder sbSourceCode = new StringBuilder();
            using ( StringWriter writer = new StringWriter(sbSourceCode) )
            {
                provider.GenerateCodeFromCompileUnit(compileUnit, writer, new CodeGeneratorOptions());
            }
            string sourceCode = sbSourceCode.ToString();

            CompilerParameters parameters = new CompilerParameters()
                {
                    GenerateInMemory = true
                };

            var assembliesToAdd =
                Template.Parameters
                    .Select(parameterCrt => parameterCrt.Value.Type.Assembly.Location)
                    .Distinct();

            parameters.ReferencedAssemblies.AddRange( 
                assembliesToAdd.ToArray()
            );
            parameters.ReferencedAssemblies.Add(
                GetType().Assembly.Location
            );
            CompilerResults results = provider.CompileAssemblyFromSource(
                parameters,
                sourceCode
            );

            if (results.Errors.HasErrors)
            {
                Debug.WriteLine(
                    "Invalid code:" + sourceCode
                );
                foreach (CompilerError errorCrt in results.Errors)
                    Debug.WriteLine(
                        "Error " + errorCrt.ErrorText + " at line " + errorCrt.Line
                    );
                throw new TemplateCompileException()
                {
                    SourceCode = sourceCode,
                    Errors = results.Errors
                };
            }

            Assembly compiledAssembly = results.CompiledAssembly;
            return compiledAssembly.GetType(GeneratedNamespace + "." + className);
        }

        IEnumerable<CodeTypeMember> ParametersCodeDoms
        {
            get
            {
                foreach (KeyValuePair<string, Parameter> parameterDefCrt in Template.Parameters)
                {
                    string parameterName = parameterDefCrt.Key;
                    Type parameterType = parameterDefCrt.Value.Type;

                    yield return MakeGetParameterProperty(parameterName, parameterType);
                }
            }
        }

        CodeMemberProperty MakeGetParameterProperty(string parameterName, Type parameterType)
        {
            CodeMemberProperty parameter = new CodeMemberProperty();
            parameter.Name = parameterName;
            parameter.Type = new CodeTypeReference(parameterType);
            parameter.GetStatements.Add(
                new CodeMethodReturnStatement(
                        new CodeCastExpression(
                            parameterType,
                            new CodePropertyReferenceExpression(
                                new CodeArrayIndexerExpression(
                                        new CodePropertyReferenceExpression(
                                            new CodePropertyReferenceExpression(new CodeThisReferenceExpression(), "Template"),
                                            "Parameters"
                                        ),
                                        new CodePrimitiveExpression(parameterName)
                                ),
                                "Value"
                            )
                        )
                )
            );

            return parameter;
        }

        const string BufferVariable = "buffer";
        
        CodeTypeMember GenerateMethodCodeDom
        {
            get
            {
                CodeMemberMethod generateMethod = new CodeMemberMethod();
                generateMethod.ReturnType = new CodeTypeReference( typeof(string) );
                generateMethod.Attributes = MemberAttributes.Public | MemberAttributes.Override;
                generateMethod.Name = "Generate";

                generateMethod.Statements.Add( 
                    new CodeVariableDeclarationStatement( 
                        typeof(StringBuilder),
                        BufferVariable,
                        new CodeObjectCreateExpression(
                            new CodeTypeReference(typeof(StringBuilder))
                        )
                    )
                );

                foreach (TemplateFragment fragmentCrt in TemplateFragments)
                    generateMethod.Statements.AddRange(fragmentCrt.Generate());

                generateMethod.Statements.Add(
                    new CodeMethodReturnStatement(
                        new CodeMethodInvokeExpression(
                                new CodeVariableReferenceExpression(BufferVariable),
                                "ToString"
                            ))
                );

                return generateMethod;
            }
        }

        IEnumerable<TemplateFragment> TemplateFragments
        {
            get
            {
                int startPosCrt = 0;
                string templateBody = Template.TemplateBody;
                while (startPosCrt < templateBody.Length)
                {
                    int nextPos = 0;

                    foreach (var attemptCrt in new Func<TemplateFragment>[] {
                                    () => ExpressionTemplateFragment.Extract( templateBody, startPosCrt, out nextPos),
                                    () => CodeTemplateFragment.Extract( templateBody, startPosCrt, out nextPos),
                                    () => ConstantTemplateFragment.Extract( templateBody, startPosCrt, out nextPos)
                                }
                        )
                    {
                        TemplateFragment crt = attemptCrt();
                        if (crt != null)
                        {
                            startPosCrt = nextPos;
                            yield return crt;
                            break;
                        }
                    }
                         
               }
            }
        }
        #endregion

        #region Template Fragment Classes
        abstract class TemplateFragment
        {
            public abstract CodeStatementCollection Generate();
            public string Body { get; set; }
        }

        const string ReplacementStart = "<%";
        const string ReplacementEnd = "%>";
        const string ExpressionMarker = "=";

        class ConstantTemplateFragment : TemplateFragment
        {
            public static ConstantTemplateFragment Extract(string templateBody, int start, out int next)
            {
                int nextReplacement = templateBody.IndexOf(ReplacementStart, start);
                if (nextReplacement == -1)
                    nextReplacement = templateBody.Length;

                next = nextReplacement;

                return new ConstantTemplateFragment() { Body = templateBody.Substring(start, next - start) };
            }

            public override CodeStatementCollection Generate()
            {
                return new CodeStatementCollection(
                    new CodeStatement[] 
                    {
                        new CodeExpressionStatement(
                            new CodeMethodInvokeExpression(
                                new CodeVariableReferenceExpression(BufferVariable),
                                "Append",
                                new CodePrimitiveExpression(Body)
                            )
                        )
                    }
                );
            }
        }

        class ExpressionTemplateFragment : TemplateFragment
        {
            public static ExpressionTemplateFragment Extract(string templateBody, int start, out int next)
            {
                string nextReplacement;
                GetReplacement(templateBody, start, out nextReplacement, out next);

                if (nextReplacement == null)
                    return null;
                else
                {
                    nextReplacement = nextReplacement.Trim();
                    if (nextReplacement.StartsWith(ExpressionMarker))
                    {
                        nextReplacement = nextReplacement.Substring(1);
                        return new ExpressionTemplateFragment() { Body = nextReplacement };
                    }
                    else
                        return null;
                }
            }

            public override CodeStatementCollection Generate()
            {
                return new CodeStatementCollection(
                    new CodeStatement[] 
                    {
                        new CodeExpressionStatement(
                            new CodeMethodInvokeExpression(
                                new CodeVariableReferenceExpression(BufferVariable),
                                "Append",
                                new CodeSnippetExpression(Body)
                            )
                        )
                    }
                );
            }   
        }

        class CodeTemplateFragment : TemplateFragment
        {
            public static CodeTemplateFragment Extract(string templateBody, int start, out int next)
            {
                string nextReplacement;
                GetReplacement(templateBody, start, out nextReplacement, out next);

                if (nextReplacement == null)
                    return null;
                else
                    return new CodeTemplateFragment() { Body = nextReplacement };
            }

            public override CodeStatementCollection Generate()
            {
                return new CodeStatementCollection(
                    new CodeStatement[] 
                    {
                        new CodeSnippetStatement(Body)
                    }
                );
            }
        }

        static void GetReplacement(string templateBody, int start, out string replacement, out int nextPosition)
        {
            if (templateBody.Substring(start).StartsWith(ReplacementStart))
            {
                int endIndex = templateBody.IndexOf(ReplacementEnd, start);
                if (endIndex == -1)
                    throw new ReplacementNotClosedException()
                        { ReplacementText = templateBody.Substring(start) };

                replacement = templateBody.Substring(
                                            start + ReplacementStart.Length,
                                            endIndex - (start + ReplacementStart.Length) );
                nextPosition = endIndex + ReplacementStart.Length;
            }
            else
            {
                nextPosition = start;
                replacement = null;
            }
        }
        #endregion
    }
}
